package jumpingnotes.codec.ffmpeg;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jumpingnotes.codec.CodecParams;
import jumpingnotes.codec.CodecService;
import jumpingnotes.codec.CodecTask;
import jumpingnotes.codec.CodecTaskListener;
import jumpingnotes.storage.StorageService;

public class FFMpegCodecService
implements CodecService, Runnable {
	private String ffmpegPath;
	private StorageService storageService;
	private Thread worker;
	private ExecutorService inputStreamExecutor = Executors.newFixedThreadPool(2);
	private List<CodecTaskListener> listeners = new CopyOnWriteArrayList<CodecTaskListener>();
	private LinkedBlockingQueue<CodecTask> taskQueue = new LinkedBlockingQueue<CodecTask>();
	volatile private boolean isStdOutRunning;
	volatile private boolean isStdErrRunning;
	private Pattern durationPattern;
	private Pattern processPattern;

	public void init() {
		durationPattern = Pattern.compile("\\s\\sDuration:\\s*(\\d+):(\\d+):(\\d+)\\.(\\d+),");
		processPattern = Pattern.compile("^size=\\s*\\d+kB\\s+time=(\\d+)\\.(\\d+)");
		worker = new Thread(this);
		worker.setName("FFMpeg Encoder");
		worker.setDaemon(true);
		worker.start();
	}	
	
	public void setFfmpegPath(String ffmpegPath) {
		this.ffmpegPath = ffmpegPath;
	}

	public void setStorageService(StorageService storageService) {
		this.storageService = storageService;
	}

	public void addListener(CodecTaskListener listener) {
		listeners.add(listener);
	}

	public void removeListener(CodecTaskListener listener) {
		listeners.remove(listener);
	}

	public CodecTask removeTask(String taskId) {
		Iterator<CodecTask> taskIter = taskQueue.iterator();
		CodecTask toRemove = null;
		while (taskIter.hasNext()) {
			CodecTask task = taskIter.next();
			if (task.getId().equals(taskId)) {
				toRemove = task;
				break;
			}
		}
		if (toRemove != null) {
			taskQueue.remove(toRemove);
		}
		return toRemove;
	}

	public boolean addTask(CodecTask task) {
		if (task.getState() != CodecTask.CODEC_TASK_STATE_INIT) {
			return false;
		}
		task.setState(CodecTask.CODEC_TASK_STATE_PENDING);
		return taskQueue.offer(task);
	}

	public void run() {
		while (true) {
			try {
				CodecTask task = taskQueue.take();
				task.setState(CodecTask.CODEC_TASK_STATE_STARTED);
				fireTaskStart(task);
				processTask(task);
				fireTaskDone(task);
			} catch (InterruptedException e) {
				// do nothing
			}
		}
	}
	
	protected void processTask(CodecTask task) {
		String inputFilename = task.getInputFilename();
		String outputFilename = task.getOutputFilename();
		// check the existence of the input file
		File fin = null;
		if (task.getInputFilePath() != null) {
			fin = new File(task.getInputFilePath());
		} else {
			fin = new File(
					storageService.getRepoDir() +
					File.separator +
					"audio" +
					File.separator +
					inputFilename
			);
			if (!fin.exists()) {
				fin = new File(
						storageService.getTmpDir() +
						File.separator +
						"audio" +
						File.separator +
						inputFilename
				);
			}
		}
		if (!fin.exists() || fin.isDirectory()) {
			task.setCode(CodecTask.CODEC_TASK_CODE_FILE_NOT_FOUND);
			task.setState(CodecTask.CODEC_TASK_STATE_DONE);
			return;
		}
		
		File fout = null;
		if (task.getOutputFilePath() != null) {
			fout = new File(task.getOutputFilePath());
		} else {
			fout = new File(
					storageService.getTmpDir() +
					File.separator +
					"audio" +
					File.separator +
					outputFilename
			);
		}
		if (fout.exists() && fout.isDirectory()) {
			task.setCode(CodecTask.CODEC_TASK_CODE_ACCESS_DENIED);
			task.setState(CodecTask.CODEC_TASK_STATE_DONE);
			return;
		}
		
		StringBuffer cmdBuf = new StringBuffer();
		cmdBuf.append(ffmpegPath).append(File.separatorChar).append("ffmpeg.exe");
		CodecParams codecParams = task.getParam();
		if (codecParams != null) {
			if (codecParams.getInputCodec() != null) {
				cmdBuf.append(" -acodec ").append(codecParams.getInputCodec());
			}
		}
		cmdBuf.append(" -y -i ").append(fin.getAbsolutePath());
		// get extra parameters from CodecParams
		if (codecParams != null) {
			cmdBuf.append(" -vol ")
			.append(codecParams.getVolume());
			cmdBuf.append(" -ss ")
			.append(codecParams.getStartTime());
			if (codecParams.getDuration() >= 0) {
				cmdBuf.append(" -t ")
				.append(codecParams.getDuration());
			}
			if (codecParams.getSizeLimit() >= 0) {
				cmdBuf.append(" -fs ")
				.append(codecParams.getSizeLimit());
			}
			if (codecParams.isCopyInputCodec()) {
				cmdBuf.append(" -acodec copy ");
			} else {
				if (codecParams.getBitRate() != 0) {
					cmdBuf.append(" -ab ")
					.append(codecParams.getBitRate())
					.append("k");
				}
				if (codecParams.getSampleRate() != 0) {
					cmdBuf.append(" -ar ")
					.append(codecParams.getSampleRate());
				}
				if (codecParams.getOutputCodec() != null) {
					cmdBuf.append(" -acodec ")
					.append(codecParams.getOutputCodec());
				}
				if (codecParams.getNumChannels() != 0) {
					cmdBuf.append(" -ac ")
					.append(codecParams.getNumChannels());
				}
			}
		}
		
		cmdBuf.append(" -vn "); // disable video
		
		// get format from output file extension
		String format = "mp3";
		int extIdx = outputFilename.lastIndexOf(".");
		if (extIdx != -1) {
			String ext = outputFilename.substring(extIdx + 1);
			if ("3gpp".equals(ext)) {
				format = "3g2";
			} else {
				format = ext;
			}
		}
		cmdBuf.append(" -f ").append(format);
		
		cmdBuf.append(" ").append(fout.getAbsolutePath());
		
		try {
			Process ffmpegProcess = Runtime.getRuntime().exec(cmdBuf.toString());
			InputStreamProcessor stdoutProcessor =
				new InputStreamProcessor(task, ffmpegProcess.getInputStream(), false);
			InputStreamProcessor stderrProcessor =
				new InputStreamProcessor(task, ffmpegProcess.getErrorStream(), true);
			isStdOutRunning = true;
			isStdErrRunning = true;
			inputStreamExecutor.execute(stdoutProcessor);
			inputStreamExecutor.execute(stderrProcessor);
			int exitVal = ffmpegProcess.waitFor();
			if (exitVal == 0) {
				task.setCode(CodecTask.CODEC_TASK_CODE_SUCCESS);
			} else {
				task.setCode(CodecTask.CODEC_TASK_CODE_GENERAL_FAILURE);
			}
			// wait for stdout and stderr processor stop running
			while (isStdOutRunning || isStdErrRunning) {
				Thread.sleep(10);
			}
			if (task.getCode() == CodecTask.CODEC_TASK_CODE_SUCCESS) {
				if (format.equals("swf") &&
						(codecParams == null || "libmp3lame".equals(codecParams.getOutputCodec()) ||
							"libmp3lame".equals(codecParams.getInputCodec()) || inputFilename.endsWith("mp3"))) {
						// FFMPEG generates wrong swf length in header
						// We need to fix the length field per file size
						fixSWFHeader(fout);
					}
			}
		} catch (Exception e) {
			task.setCode(CodecTask.CODEC_TASK_CODE_GENERAL_FAILURE);
			task.setErrorInfo(e.getMessage());
		} finally {
			task.setState(CodecTask.CODEC_TASK_STATE_DONE);
		}
	}
	
	protected void fireTaskStart(CodecTask task) {
		for (CodecTaskListener listener : listeners) {
			listener.onTaskStart(task);
		}
	}
	
	protected void fireTaskDone(CodecTask task) {
		for (CodecTaskListener listener : listeners) {
			listener.onTaskDone(task);
		}
	}
	
	protected void fireTaskProgress(CodecTask task, int percentage) {
		for (CodecTaskListener listener : listeners) {
			listener.onTaskProgress(task, percentage);
		}
	}
	
	private class InputStreamProcessor implements Runnable {
		private CodecTask task;
		private InputStream input;
		private boolean isStdErr;
		private int startTime = 0;
		private int duration = -1;
		private int lastPercentage = -1;
		
		public InputStreamProcessor(
				CodecTask task, InputStream input, boolean isStdErr) {
			this.task = task;
			this.input = input;
			this.isStdErr = isStdErr;
		}

		public void run() {
			try {
				CodecParams params = task.getParam();
				if (params != null) {
					if (params.getDuration() >= 0) {
						duration = params.getDuration();
					}
					startTime = params.getStartTime();
				}
				StringBuffer strBuf = new StringBuffer();
				byte[] buf = new byte[256];
				int bytesRead = 0;
				boolean gotReturn = false;
				while ((bytesRead = input.read(buf)) >= 0) {
					for (int i = 0; i < bytesRead; i++) {
						char c = (char) buf[i];
						switch (c) {
							case '\n':
								onLine(strBuf.toString());
								strBuf = new StringBuffer();
								gotReturn = false;
								break;
							case '\r':
								if (gotReturn) {
									strBuf.append('\r');
								} else {
									gotReturn = true;
								}
								break;
							default:
								if (gotReturn) {
									onLine(strBuf.toString());
									strBuf = new StringBuffer();
									gotReturn = false;
								}
								strBuf.append(c);
								break;
						}
					}
				}
			} catch (IOException e) {
				
			} finally {
				if (isStdErr) {
					isStdErrRunning = false;
				} else {
					isStdOutRunning = false;
				}
			}
		}
		
		private void onLine(String line) {
			if (isStdErr) {
				if (line.startsWith("  Duration")) {
					int inputDuration = getInputDuration(line);
					if (inputDuration >= 0) {
						if (duration < 0) {
							duration = inputDuration - startTime;
						} else {
							// use the smaller duration
							duration = duration < inputDuration - startTime ?
									duration : inputDuration - startTime;
						}
					} else {
						// use user-specified duration
					}
				}
				if (line.startsWith("size=")) {
					int currentSecond = getCurrentSecond(line);
					if (duration > 0) {
						int percentage = currentSecond * 100 / duration;
						if (percentage > 100) {
							percentage = 100;
						}
						if (lastPercentage < percentage) {
							fireTaskProgress(task, percentage);
							lastPercentage = percentage;
						}
					}
				}
			} else {
				System.out.println(line);
			}
		}
	}
	
	private int getInputDuration(String line) {
		Matcher matcher = durationPattern.matcher(line);
		if (matcher.find()) {
			int hour = Integer.parseInt(matcher.group(1));
			int minute = Integer.parseInt(matcher.group(2));
			int second = Integer.parseInt(matcher.group(3));
			return hour * 3600 + minute * 60 + second;
		} else {
			return -1;
		}
	}
	
	private int getCurrentSecond(String line) {
		Matcher matcher = processPattern.matcher(line);
		if (matcher.find()) {
			return Integer.parseInt(matcher.group(1));
		} else {
			return 0;
		}
	}
	
	private void fixSWFHeader(File swfFile) {
		RandomAccessFile randomAccessSwf = null;
		try {
			randomAccessSwf = new RandomAccessFile(swfFile, "rw");
			long fileSize = randomAccessSwf.length();
			if (fileSize < 0x100000000L) {
				randomAccessSwf.seek(4);
				randomAccessSwf.writeByte((int) (fileSize & 0xff));
				randomAccessSwf.writeByte((int) ((fileSize >> 8) & 0xff));
				randomAccessSwf.writeByte((int) ((fileSize >> 16) & 0xff));
				randomAccessSwf.writeByte((int) ((fileSize >> 24) & 0xff));
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (randomAccessSwf != null) {
				try {
					randomAccessSwf.close();
				} catch (IOException e) {}
			}
		}
	}
}
